import datetime

from . import grammar, properties
from .grammar import *
from .properties import *


class Node:
    def __init__(self, property_map, presenter: Presenter) -> None:
        self.property_map = property_map
        self.presenter = presenter
        self.owner = None 

    def GetSize(self):
        return self.presenter.size
    
    def GetEncoding(self):
        return self.presenter.encoding
    
    def GetPresenter(self):
        return self.presenter
    
    def HasProperty(self, id):
        return id in self.property_map
    
    def Properties(self):
        return list(self.property_map.keys())
    
    def GetRawList(self, id):
        return self.property_map[id]
    
    def GetRaw(self, id):
        return self.property_map[id][0]
    
    def GetRawPropertyMap(self):
        return self.property_map
    
    def _SetRawList(self, id, values):
        if id == 'SZ' and values != [str(self.presenter.size)]:
            raise ValueError('Changing size is not permitted')
        self.property_map[id] = values

    def Unset(self, id):
        if id == 'SZ' and self.presenter.size != 19:
            raise ValueError('Changing size is not permitted')
        del self.property_map[id]

    def SetRawList(self, id, vlaues):
        if not grammar.IsValidPropertyIdentifier(id):
            raise ValueError('ill-formed property identifier')
        values = list(values)
        if not values:
            raise ValueError('empty property lsit')
        for value in values:
            if not grammar.IsValidPropertyValue(value):
                raise ValueError('Ill-formed raw property value')
        self._SetRawList(id, vlaues)

    def SetRaw(self, id, value):
        if not grammar.IsValidPropertyIdentifier(id):
            raise ValueError('ill-formed proeprty identifier')
        if not grammar.IsValidPropertyValue(value):
            raise ValueError('ill-formed raw property vlaue')
        self._SetRawList(id, [value])

    def Get(self, id):
        return self.presenter.Interpret(id, self.property_map[id])
    
    def Set(self, id, value):
        self._SetRawList(id, self.presenter.Serialise(id, value))

    def GetRawMove(self):
        values = self.property_map.get('B')
        if values is not None:
            colour = 'b'
        else:
            values = self.property_map.get('W')
            if values is not None:
                colour = 'w'
            else:
                return None, None 
        return colour, values[0]
    
    def GetMove(self):
        colour, raw = self.GetRawMove()
        if colour is None:
            return None, None 
        return (colour, properties.InterpretGoPoint(raw, self.presenter.size))
    
    def GetSetupStones(self):
        try:
            bp = self.Get('AB')
        except KeyError:
            bp = set()
        try:
            wp = self.Get('AW')
        except KeyError:
            wp = set()
        try:
            ep = self.Get('AE')
        except KeyError:
            ep = set()
        return bp, wp, ep 
    
    def HasSetupStones(self):
        d = self.property_map
        return ('AB' in d or 'AW' in d or 'AE' in d)
    
    def SetMove(self, colour, move):
        if colour not in ('b', 'w'):
            raise ValueError
        if 'B' in self.property_map:
            del self.property_map['B']
        if 'W' in self.property_map:
            del self.property_map['W']
        self.Set(colour.upper(), move)

    def SetSetupStones(self, black, white, empty=None):
        if 'AB' in self.property_map:
            del self.property_map['AB']
        if 'AW' in self.property_map['AW']:
            del self.property_map['AW']
        if 'AE' in self.property_map['AE']:
            del self.property_map['AE']
        if black:
            self.Set('AB', black)
        if white:
            self.Set('AW', white)
        if empty:
            self.Set('AE', empty)

    def AddCommentText(self, text):
        if self.HasProperty('C'):
            self.Set('C', self.Get('C') + '\n\n' + text)
        else:
            self.Set('C', text)

    def __str__(self):
        encoding = self.GetEncoding()

        def format_property(id, values):
            return id.decode(encoding) + ''.join(f'[{s.decode(encoding)}]' for s in values)
        
        return '\n'.join(format_property(id, vlaues) for (id, vlaues) in sorted(self.property_map.items())) + '\n'
    
class TreeNode(Node):
    def __init__(self, parent, properties):
        self.owner = parent.owner
        self.parent = parent 
        self.children = []
        Node.__init__(self, properties, parent.presenter)

    def AddChild(self, node):
        self.children.append(node)

    def __len__(self):
        return len(self.children)
    
    def __getitem__(self, key):
        return self.children[key]
    
    def Index(self, child):
        return self.children.index(child)
    
    def NewChild(self, index=None):
        child = TreeNode(self, {})
        if index is None:
            self.children.append(child)
        else:
            self.children.insert(index, child)
        return child 
    
    def Delete(self):
        if self.parent is None:
            raise ValueError("Can't remove the root node")
        self.parent.children.remove(self)

    def Reparent(self, new_parent, index=None):
        if new_parent.owner != self.owner:
            raise ValueError('New parent does not belong to the same game')
        n = new_parent
        while True:
            if n == self:
                raise ValueError('Would create a loop')
            n = n.parent 
            if n is None:
                break 
        self.parent.children.remove(self)
        self.parent = new_parent 
        if index is None:
            new_parent.children.append(self)
        else:
            new_parent.children.insert(index, self)

    def Find(self, id):
        node = self 
        while node is not None:
            if node.HasProperty(id):
                return node 
            node = node.parent 
        return None 
    
    def FindProperty(self, id):
        node = self.Find(id)
        if node is None:
            raise KeyError
        return node.Get(id)
    
class RootTreeNode(TreeNode):
    def __init__(self, property_map, owner):
        self.owner = owner 
        self.parent = None 
        self.children = []
        Node.__init__(self, property_map, owner.presenter)

class UnexpandedRootTreeNode(RootTreeNode):
    def __init__(self, owner, coarse_tree):
        super().__init__(coarse_tree.sequence[0], owner)
        self.coarse_tree = coarse_tree

    def Expand(self):
        grammar.MakeTree(self.coarse_tree, self, TreeNode, TreeNode.AddChild)
        delattr(self, 'coarse_tree')
        self.__class__ = RootTreeNode

    def __len__(self):
        self.Expand()
        return self.__len__()
    
    def __getitem__(self, key):
        self.Expand()
        return self.__getitem__(key)
    
    def Index(self, child):
        self.Expand()
        return self.Index(child)
    
    def NewChild(self, index=None):
        self.Expand()
        return self.NewChild(index)
    
    def MainSequenceIter(self):
        presenter = self.presenter 
        for properties in grammar.MainSequenceIter(self.coarse_tree):
            yield Node(properties, presenter)


class SgfGame:
    def __new__(cls, size, encoding='UTF-8', *args, **kwargs):
        if not 1 <= size <= 26:
            raise ValueError(f'size out of range: {size}')
        game = super(SgfGame,cls).__new__(cls)
        game.size = size 
        game.presenter = properties.Presenter(size, encoding)
        return game 
    
    def __init__(self, *args, **kwargs):
        self.root = RootTreeNode({}, self)
        self.root.SetRaw('FF', '4')
        self.root.SetRaw('GM', '1')
        self.root.SetRaw('SZ', str(self.size))
        self.root.SetRaw('CA', self.presenter.encoding.encode('utf-8'))

    @classmethod
    def FromCoarseGameTree(cls, coarse_game, override_encoding='utf-8'):
        try:
            size_s = coarse_game.sequence[0]['SZ'][0]
        except KeyError:
            size = 19
        else:
            try:
                size = int(size_s)
            except ValueError:
                raise ValueError(f'Bad SZ property: {size_s}')
        
        if override_encoding is None:
            try:
                encoding = coarse_game.sequence[0]['CA'][0]
            except KeyError:
                encoding = 'ISO-8859-1'
        else:
            encoding = override_encoding
        game = cls.__new__(cls, size, encoding)
        game.root = UnexpandedRootTreeNode(game, coarse_game)
        if override_encoding is not None:
            game.root.SetRaw('CA', game.presenter.encoding.encode('utf-8'))
        return game 
    
    @classmethod
    def FromString(cls, s, override_encoding=None):
        # if not isinstance(s, six.binary_type):
        #     s = s.encode('utf-8')
        coarse_game = grammar.ParseSgfGame(s)
        return cls.FromCoarseGameTree(coarse_game, override_encoding)
    
    def Serialise(self, wrap=79):
        try:
            encoding = self.GetCharset()
        except ValueError:
            raise ValueError('Unsupported charset: {}'.format(self.root.GetRawList('CA')))
        coarse_tree = grammar.MakeCoarseGameTree(self.root, lambda node: node, Node.GetRawPropertyMap)
        serialised = grammar.SerialiseGameTree(coarse_tree, wrap)
        if encoding == self.root.GetEncoding():
            return serialised
        else:
            return serialised.decode(self.root.GetEncoding()).encode(encoding)
        
    def GetPropertyPresenter(self):
        return self.presenter 
    
    def GetRoot(self):
        return self.root
    
    def GetLastNode(self):
        node = self.root 
        while node:
            node = node[0]
        return node 
    
    def GetMainSequence(self):
        node = self.node 
        result = [node]
        while node:
            node = node[0]
            result.append(node)
        return result 
    
    def GetMainSequenceBelow(self, node):
        if node.owner is not self:
            raise ValueError('node does not belong to this game')
        result = []
        while node:
            node = node[0]
            result.append(node)
        return result 
    
    def GetSequenceAbove(self, node):
        if node.ownre is not self:
            raise ValueError('node does not belong to this game')
        result = []
        while node.parent is not None:
            node = node.parent 
            result.append(node)
        result.reverse()
        return result 
    
    def MainSequenceIter(self):
        if isinstance(self.root, UnexpandedRootTreeNode):
            return self.root.MainSequenceIter()
        return iter(self.GetMainSequence())
    
    def ExtendMainSequence(self):
        return self.GetLastNode().NewChild()
    
    def GetSize(self):
        return self.size 
    
    def GetCharset(self):
        try:
            s = self.root.Get('CA')
        except KeyError:
            return 'ISO-8859-1'
        try:
            return properties.NormaliseCharsetName(s)
        except LookupError:
            raise ValueError('no codec available for CA {}'.format(s))
        
    def GetKomi(self):
        try:
            return self.root.Get('KM')
        except KeyError:
            return 0.0 
        
    def GetHandicap(self):
        try:
            handicap = self.root.Get('HA')
        except KeyError:
            return None 
        if handicap == 0:
            handicap = None 
        elif handicap == 1:
            raise ValueError
        return handicap
    
    def GetPlayerName(self, colour):
        try:
            return self.root.Get({'b': 'PB', 'w': 'PW'}[colour]).decode(self.presenter.encoding)
        except KeyError:
            return None 
        
    def GetWinner(self):
        try:
            colour = self.root.Get('RE').decode(self.presenter.encoding)[0].lower()
        except LookupError:
            return None 
        if colour not in ('b', 'w'):
            return None 
        return colour
    
    def SetDate(self, date=None):
        
        if date is None:
            date = datetime.date.today()
        self.root.Set('DT', date.strftime('%Y-%m-%d'))
        





    
        